/*
 * $Id$
 */
package corejs.rt;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import static corejs.rt.Invocable.*;

public class Array extends Obj implements ArrayAccessSupport {
   
   private List<Value> _vals;
   private Int _length;
   
   public Array() {
      this(new ArrayList<Value>());
   }

   public Array(final List<Value> vals) {
      this(prototype, vals);
   }

   Array(final Value proto) {
      this(proto, new ArrayList<Value>());
   }

   private Array(final Value proto, final List<Value> vals) {
      super(proto);
     _vals = vals;
     store("length", _length = new Int(_vals.size()));
   }

   /**
    * 
    * @return 
    */
   public Value pop() {
      final Value o = _vals.remove(_vals.size() - 1);
      _length.setValue(_vals.size());
      return o;
   }
   
   /**
    * 
    * @param v
    * @return 
    */
   public Array push(final Value v) {
     _vals.add(v);
     _length.setValue(_vals.size());
     return this;
   }

   public Array concat(final Value... others) {
      final List<Value> li = new ArrayList<Value>(_vals);
      for (final Value v : others) {
         if (v instanceof Array) {
            li.addAll(((Array)v)._vals);
         }
         else {
            li.add(v);
         }
      }
      return new Array(li);
   }

   /**
    * 
    * @param fn
    * @return 
    */
   public Array map(final ExecutionContext ec, final Invocable fn) {
      final List<Value> li = new ArrayList<Value>();
      final String argName = fn.getArgumentNames()[0];
      for (final Value o : _vals) {
         li.add(fn.invoke(ec.put(argName, o)));
      }
      return new Array(li);
   }

   /**
    * 
    * @param sep
    * @return 
    */
   public Str join(final ExecutionContext ec, final String sep) {
      final StringBuilder sb = new StringBuilder();
      int i = 0;
      for (; i < _vals.size() - 1; ++i) {
         sb.append(ec.invoke(_vals.get(i), "toString")).append(sep);
      }

      if (i < _vals.size()) {
         sb.append(ec.invoke(_vals.get(i), "toString"));
      }
      
      return new Str(sb.toString());
   }

   public int length() {
      return _vals.size();
   }

   @Override
   public Value resolve(final int index) {
      final int s = _vals.size();
      if (index < 0) {
         return Math.abs(index) >= s ? Undefined.UNDEFINED
            :                          _vals.get(s + index);
      }
      else {
         return index >= s ? Undefined.UNDEFINED : _vals.get(index);
      }
   }
   
   public Array set(final int index, final Value v) {
      _vals.set(index, v);
      return this;
   }
   
   /**
    *
    * @param callback
    * @param thisArg
    * @return
    */
   public Array forEach(final ExecutionContext ec,
                        final Invocable callback, 
                        final Value thisArg) {
      final int sz = _vals.size();
      for (int i = 0; i < sz; ++i) {
         ec.invoke(thisArg.isUndefined() ? null : thisArg, 
                   callback, _vals.get(i), Int.valueOf(i), this);
      }
      
      return this;      
   }
   
   /**
    * 
    * @param callback
    * @param thisArg
    * @return 
    */
   public Array filter(final ExecutionContext ec,
                       final Invocable callback,
                       final Value thisArg) {   
      final Array array = new Array();
      for (int i = 0; i < _vals.size(); ++i) {
         final Value ths = thisArg.isUndefined() ? null : thisArg;
         final Value v = ec.invoke(ths, callback, 
                                   _vals.get(i), Int.valueOf(i), this);
         if (v.toBool().isTrue()) {
            array.push(_vals.get(i));
         }
      }
      return array;
   }
   
   /**
    * 
    * @return 
    */
   public Array reverse() {
      final int sz = _vals.size(), m = sz / 2;
      for (int i = 0; i < m; ++i) {
         final int j = sz - 1 - i;
         final Value v = _vals.get(i);
         _vals.set(i, _vals.get(j));
         _vals.set(j, v);
      }
      return this;
   }
   
   /**
    * 
    * @param compareFn
    * @return 
    */
   public Array sort(final ExecutionContext ec, final Value compareFn) {
      if (compareFn.isUndefined()) {
         Collections.sort(_vals, new Comparator<Value>() {
            @Override
            public int compare(final Value v1, final Value v2) {
               return ec.toStr(v1).compareTo(ec.toStr(v2));
            }
         });
      }
      else {
         final Invocable fn = compareFn.cast(Invocable.class);
         Collections.sort(_vals, new Comparator<Value>() {
            @Override
            public int compare(final Value o1, final Value o2) {
               return Value.toPrimitive(ec.invoke(null, fn, 
                                                  ec.toStr(o1), ec.toStr(o2)),
                                        ec).toNumber().intValue();
            }
         });
      }
      return this;
   }

   /**
    *
    * @param el
    * @param from
    * @return
    */
   public Int indexOf(final Value el, final int from) {
      final int sz = _vals.size();      
      int start = from < 0 ? from + sz : from;
      if (start < 0) {
         start = 0;
      }
      for (int i = start; i < sz; ++i) {
         if (_vals.get(i).equalsStrictly(el)) {
            return Int.valueOf(i);
         }
      }
      return Int.valueOf(-1);
   }
   
   /**
    *
    * @param el
    * @param from
    * @return
    */
   public Int lastIndexOf(final Value el, final int from) {
      final int sz = _vals.size();
      int start = (from >= sz ? sz - 1 : from);
      if (start < 0) {
         start += sz;
      }
      for (int i = start; i >= 0; --i) {
         if (_vals.get(i).equalsStrictly(el)) {
            return Int.valueOf(i);
         }
      }
      return Int.valueOf(-1);
   }
   
   /**
    *
    * @param ec
    * @param fn
    * @param init
    * @return
    */
   public Value reduce(final ExecutionContext ec, final Invocable fn, 
                       final Value init, final boolean leftToRight) {
      final int sz = _vals.size();
      if (init.isUndefined()) {
         if (0 == sz) {
            throw new TypeError();
         }
         else if (sz == 1) {
            return _vals.get(0);
         }
      }
      else if (sz == 0) {
         return init;
      }
      
      final int start = init.isUndefined() ? 1 : 0;
      Value prev = init.isUndefined() ? (leftToRight ? _vals.get(0) 
                                                     : _vals.get(sz - 1)) 
                                      : init;
      
      for (int i = start; i < sz; ++i) {
         final int j = (leftToRight ? i : sz - i - 1);
         prev = ec.invoke(null, fn, prev, _vals.get(j), Int.valueOf(j), this);
      }      
      return prev;
   }
   
   /**
    *
    * @param ec
    * @param fn
    * @param thisArg
    * @return
    */
   public Bool every(final ExecutionContext ec, final Invocable fn, 
                     final Value thisArg, final boolean b) {
      final int sz = _vals.size();
      for (int i = 0; i < sz; ++i) {
         final Bool v = ec.invoke(thisArg.isUndefined() ? null : thisArg, 
                                  fn, resolve(i), Int.valueOf(i), this).toBool();
         if (b && !v.isTrue()) {
            return Bool.FALSE;
         }
         else if (!b && v.isTrue()) {
            return Bool.TRUE;
         }
      }
      return Bool.valueOf(b);             
   }
   
   // Array.prototype
   static final Array prototype = new Array(Obj.prototype);
   
   static void _array_init_prototype_() {
      prototype.store("push", new F1() {
         @Override
         public Array invoke(final ExecutionContext ec) {
            return ec.get(Array.class).push(ec.get(_arg_));
         }
      });
     
      final String[] _concat_args_ = {"*"};
      prototype.store("concat", new Invocable() {
         @Override
         public String[] getArgumentNames() { return _concat_args_; }

         @Override
         public Array invoke(final ExecutionContext ec) {
            final Arguments arguments = ec.get("arguments", Arguments.class);
            final int sz = arguments.length();
            final Value[] args = new Value[sz];
            for (int i = 0; i < sz; ++i) {
               args[i] = arguments.resolve(i);
            }
            return ec.get(Array.class).concat(args);
         }
      });

      prototype.store("map", new F2() {
         @Override
         public Array invoke(final ExecutionContext ec) {
            return ec.get(Array.class).map(ec, ec.get(_arg1_, Invocable.class));
         }
      });

      prototype.store("pop", new Invocable() {
         @Override
         public Value invoke(final ExecutionContext ec) {
            return ec.get(Array.class).pop();
         }
      });

      prototype.store("toString", new Invocable() {
         @Override
         public Str invoke(final ExecutionContext ec) {
            return new Str(new StringBuilder("[")
               .append(ec.get(Array.class).join(ec, ",")).append("]").toString());
         }
      });


      prototype.store("join", new F1() {
         @Override
         public Str invoke(final ExecutionContext ec) {
            return ec.get(Array.class).join(ec, ec.get(_arg_).toString());
         }
      });
      
      // forEach
      prototype.store("forEach", new F2() {
         @Override
         public Value invoke(final ExecutionContext ec) {
            return ec.get(Array.class)
                    .forEach(ec, ec.get(_arg1_, Invocable.class), ec.get(_arg2_));
         }
      });
      
      // filter
      prototype.store("filter", new F2() {
         @Override
         public Value invoke(final ExecutionContext ec) {
            final Interpreter it = Interpreter.get();
            return ec.get(Array.class)
                    .filter(ec, ec.get(_arg1_, Invocable.class), ec.get(_arg2_));
         }
      });
      
      // reverse
      prototype.store("reverse", new Invocable() {
         @Override
         public Value invoke(final ExecutionContext ec) {
            return ec.get(Array.class).reverse();
         }
      });
      
      // sort
      prototype.store("sort", new F1() {
         @Override
         public Value invoke(final ExecutionContext ec) {
            return ec.get(Array.class).sort(ec, ec.get(_arg_));
         }
      });

      // indexOf
      prototype.store("indexOf", new F2() {
         @Override
         public Value invoke(final ExecutionContext ec) {
            return ec.get(Array.class)
               .indexOf(ec.get(_arg1_), ec.getNumber(_arg2_, 0).intValue());
         }
      });
      
      // lastIndexOf
      prototype.store("lastIndexOf", new F2() {
         @Override
         Value invoke(final ExecutionContext ec) {
            final Array array = ec.get(Array.class);
            final Number start = ec.getNumber(_arg2_, array._vals.size() - 1);
            return array.lastIndexOf(ec.get(_arg1_), start.intValue());
         }
      });
      
      // reduce
      prototype.store("reduce", new F2() {
         @Override
         Value invoke(final ExecutionContext ec) {
            return ec.get(Array.class)
               .reduce(ec, ec.get(_arg1_, Invocable.class), ec.get(_arg2_), true);
         }
      });

     // reduceRight
     prototype.store("reduceRight", new F2() {
        @Override
        Value invoke(final ExecutionContext ec) {
           return ec.get(Array.class)
              .reduce(ec, ec.get(_arg1_, Invocable.class), ec.get(_arg2_), false);
        }
     });
     
     // every
     prototype.store("every", new F2() {
        @Override
        Bool invoke(final ExecutionContext ec) {
           return ec.get(Array.class)
              .every(ec, ec.get(_arg1_, Invocable.class), ec.get(_arg2_), true);
        }
     });

     // every
     prototype.store("some", new F2() {
        @Override
        Bool invoke(final ExecutionContext ec) {
           return ec.get(Array.class)
              .every(ec, ec.get(_arg1_, Invocable.class), ec.get(_arg2_), false);
        }
     });  
   } // _END_OF: Array.prototype


   public static final Invocable _constructor_ = new Invocable() {
      {
         store("isArray", new F1() {
            @Override
            public Bool invoke(final ExecutionContext ec) {
               return Bool.valueOf(ec.get(_arg_) instanceof Array);
            }
         });

         store("prototype", Array.prototype);
      }

      @Override
      public Value invoke(final ExecutionContext ec) {
         throw new UnsupportedOperationException("Not supported yet."); 
      }
   };
}
